package com.hotkey.app;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import org.jnativehook.GlobalScreen;
import org.jnativehook.NativeHookException;
import org.jnativehook.keyboard.NativeKeyEvent;
import org.jnativehook.keyboard.NativeKeyListener;
import org.slf4j.LoggerFactory;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * 监听键盘和鼠标按键, 这个是强制顺序和不支持重复的
 * 如果要支持重复就得使用List保存快捷键, 并给每个编键去配置标志位, 或者快捷键数组使用对象数组
 * 如果要不要求顺序, 就按下时候直接标志位变成true即可
 *
 * @author codor
 * @date 2023/12/08 16:23
 * @see org.jnativehook.example.NativeHookDemo
 */
public class NativeApplication extends JFrame implements NativeKeyListener {

    // ----------------------------------------------------------------------------------------------------------------- 逻辑使用

    /**
     * 快捷键组合
     * 名字也可以不写上去, 使用NativeKeyEvent.getKeyText(event.getKeyCode())获取
     */
    private Map<Integer, String> hotKeys;

    /**
     * 键编码的列表, 因为要保证顺序
     */
    private List<Integer> codes;


    /**
     * 快捷键是否按下标标志位
     */
    private Map<Integer, Boolean> flags;

    /**
     * 窗口是否展示出来了
     */
    private boolean isWindowShow = false;

    // ---------------------------------------------------------------------------------------------------------------- 界面使用

    /**
     * 倒计时
     */
    private Timer countdownTimer;

    /**
     * 倒计时时长(秒)
     */
    private final static Integer COUNT_DOWN_SECONDS = 10;

    /**
     * 提示窗口宽度
     */
    private final static Integer WARN_WINDOW_WIDTH = 900;

    /**
     * 提示窗口高度
     */
    private final static Integer WARN_WINDOW_HEIGHT = 500;

    /**
     * 发送请求超时
     */
    private final static Integer TIME_OUT = 4000;

    /**
     * 播放声音
     */
    private Clip clip;

    /**
     * 日志
     */
    private final static org.slf4j.Logger log = LoggerFactory.getLogger(NativeApplication.class);


    /**
     * 初始化标志位, 键从下方中找
     *
     * @see org.jnativehook.keyboard.NativeKeyEvent
     */
    public void initHotKeys() {
        this.hotKeys = new LinkedHashMap<>();
        this.hotKeys.put(1, "ESC");
        this.hotKeys.put(60, "F2");

        // 初始化键编码
        this.codes = new ArrayList<>();
        this.hotKeys.forEach((k, v) -> this.codes.add(k));

        // 初始化标指位
        this.initFlags();
    }

    /**
     * 初始化标志位
     */
    public void initFlags() {
        if (this.hotKeys == null) {
            this.initHotKeys();
        }
        this.flags = new HashMap<>();
        for (Integer code : this.codes) {
            this.flags.put(code, false);
        }
    }

    /**
     * 按下键时候, 强制要求顺序
     *
     * @param code 键编码
     * @return 是否全部按下了, true是, 反之false
     */
    @Deprecated
    public boolean pressedKeyOrder(int code) {
        // 如果未在内, 重置标志位
        if (!this.codes.contains(code)) {
            this.initFlags();
            return false;
        }

        for (Integer k : this.codes) {
            // 循环到 第一个非按下状态的就跳出循环或退出方法
            if (!this.flags.get(k)) {
                // 并且等于按下的键, 设置按下成功
                if (k.equals(code)) {
                    this.flags.put(code, true);
                    break;
                } else { // 不是不等于按下的, 说明按了后面的,
                    this.initFlags();
                    return false;
                }
            }
        }

        // 只有break时候到这里, 判断是否循环完, 也即code是否最后一个
        return code == this.codes.get(this.codes.size() - 1);
    }

    /**
     * 按下键时候, 要求全部按下即可
     *
     * @param code 键编码
     * @return 是否全部按下了, true是, 反之false
     */
    public boolean pressedKeyNoOrder(int code) {
        // 如果未在内, 重置标志位(只能按那几个, 那几个期间不可按其他的)
        if (!this.codes.contains(code)) {
            this.initFlags();
            return false;
        }

        // 设置按下状态
        this.flags.put(code, true);

        for (Integer k : this.codes) {
            // 如果有没按下的就跳出
            if (!this.flags.get(k)) {
                return false;
            }
        }

        // 到这里代表codes中都是按下的
        return true;
    }

    /**
     * 松开键
     */
    public void releaseKey(int code) {
        this.flags.put(code, false);
    }

    public String getKeyTexts() {
        if (CollUtil.isEmpty(this.codes)) {
            return "";
        }
        StringBuilder resB = new StringBuilder(" ");
        for (Integer code : this.codes) {
            resB.append(NativeKeyEvent.getKeyText(code)).append(" ");
        }
        return resB.toString();
    }

    /**
     * 展示警告弹窗
     *
     * @param hotKeyTexts 快捷键组合
     */
    public void showWindow(String hotKeyTexts) {

        // 右上角按钮
        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        // 窗口无装饰, 但是标题也会干掉
        setUndecorated(true);
        // 布局
        setLayout(new BorderLayout());
        // 大小
        setSize(WARN_WINDOW_WIDTH, WARN_WINDOW_HEIGHT);
        // 将窗口置于屏幕中央
        setLocationRelativeTo(null);
        // 置顶
        setAlwaysOnTop(true);

        // 提示
        String titleStr = "<html><center><br>您按下了快捷键: " + hotKeyTexts +
                "<br><br>如误操作请在倒计时之前取消！！！</center><html>";

        // 字体
        Font font = new Font("微软雅黑", Font.BOLD, 30);
        Font bigFont = new Font("微软雅黑", Font.BOLD, 40);

        // 弹窗和按钮字体
        UIManager.put("OptionPane.messageFont", font);
        UIManager.put("OptionPane.buttonFont", font);

        // 标题部分
        JLabel titleLabel = new JLabel(titleStr, JLabel.CENTER);
        titleLabel.setSize(WARN_WINDOW_WIDTH, WARN_WINDOW_HEIGHT / 5 * 3);
        titleLabel.setForeground(Color.RED);
        titleLabel.setFont(font);

        // 倒计时
        JLabel countdownLabel = new JLabel("倒计时: " + COUNT_DOWN_SECONDS + " 秒", JLabel.CENTER);
        countdownLabel.setSize(WARN_WINDOW_WIDTH, WARN_WINDOW_HEIGHT / 5);
        countdownLabel.setFont(bigFont);

        // 取消按钮
        JButton cancelButton = new JButton("取消");
        cancelButton.setSize(WARN_WINDOW_WIDTH, WARN_WINDOW_HEIGHT / 5);
        cancelButton.setFont(font);
        // 点击事件
        cancelButton.addActionListener(e -> {
            // 点击取消按钮时的处理
            if (countdownTimer != null) {
                // 停止计时
                countdownTimer.stop();
                // 重置标志位
                this.initFlags();
                // 关闭窗口
                this.closeWindow();
                log.info("手动点击取消按钮结束");
            }
        });

        // 添加到页面
        add(titleLabel, BorderLayout.NORTH);
        add(countdownLabel, BorderLayout.CENTER);
        add(cancelButton, BorderLayout.SOUTH);

        // 取消按钮焦点
        cancelButton.setFocusPainted(false);

        // 禁用通过键盘空格激活按钮
        Action blankAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
            }
        };
        ActionMap actionMap = cancelButton.getActionMap();
        actionMap.put("pressed", blankAction);
        actionMap.put("released", blankAction);

        // 窗口可见
        setVisible(true);

        // 倒计时
        AtomicInteger second = new AtomicInteger(COUNT_DOWN_SECONDS);
        countdownTimer = new Timer(1000, e -> {
            second.getAndDecrement();

            if (second.get() >= 0) {
                String countdownText = "倒计时: " + second.get() + " 秒";
                countdownLabel.setText(countdownText);
                log.info(countdownText);
            } else {
                // 计时结束提醒信息
                countdownLabel.setText("计时结束确认发送, 开始发送...");
                log.info("计时结束确认发送, 开始发送...");

                // 停止计时, 因为下方worker不会阻塞(如果直接跑上一条不会立即更改)
                countdownTimer.stop();

                // 关闭声音
                this.stopSound();

                // 专门设计用于在后台线程执行长时间运行任务而不会阻塞事件调度线程（EDT）的工具
                SwingWorker<String, Void> worker = new SwingWorker<String, Void>() {
                    @Override
                    protected String doInBackground() {
                        // 执行耗时的任务，例如HttpUtil.post
                        return HttpUtil.post("http://localhost/hotkey/press",
                                "{\"token\": \"d4k69qruHX84OcRYotLMAXq5jHe3pfYfDLVuqF9X8XNk6pB1M3Y2QK\"}", TIME_OUT);
                    }

                    @Override
                    protected void done() {
                        try {
                            String resStr = get();
                            log.info("计时结束确认发送, 发送结果" + resStr);
                            if (JSONUtil.parseObj(resStr).getInt("code", 0) != 200) {
                                JOptionPane.showMessageDialog(NativeApplication.this, "服务器返回失败: 请联系管理员: " + resStr,
                                        "警告", JOptionPane.WARNING_MESSAGE);
                            } else {
                                JOptionPane.showMessageDialog(NativeApplication.this, "计时结束确认发送, 发送成功",
                                        "消息", JOptionPane.INFORMATION_MESSAGE);
                            }
                        } catch (Exception ex) {
                            ByteArrayOutputStream baoStream = new ByteArrayOutputStream();
                            ex.printStackTrace(new PrintStream(baoStream));
                            log.error(baoStream.toString());
                            String exceptionMessage = "发送过程异常, 请联系管理员: " + ex.getMessage();
                            log.info(exceptionMessage);
                            JOptionPane.showMessageDialog(NativeApplication.this, exceptionMessage,
                                    "错误", JOptionPane.WARNING_MESSAGE);
                        }

                        // 指令发送后重置标志位
                        NativeApplication.this.initFlags();

                        // 关闭窗口
                        NativeApplication.this.closeWindow();
                    }
                };

                // 启动SwingWorker
                worker.execute();
            }
        });

        // 开始倒计时
        countdownTimer.start();
    }

    public void closeWindow() {
        dispose(); // 关闭窗口
        setVisible(false); // 设置窗口不可见
        getContentPane().removeAll(); // 移除所有组件
        revalidate(); // 重新验证布局
        repaint(); // 重绘窗口

        // 关闭窗口时候重置
        isWindowShow = false;

        // 关闭窗口时候停止声音
        this.stopSound();
    }

    /**
     * 弹窗警告, 只有在重复运行和双击jar包运行时候(都在main方法判断的)使用
     *
     * @param text 弹窗内容
     */
    public static void dialogWindow(String text) {
        Font font = new Font("微软雅黑", Font.BOLD, 30);
        UIManager.put("OptionPane.messageFont", font);
        UIManager.put("OptionPane.buttonFont", font);
        JOptionPane.showMessageDialog(new NativeApplication(), text,
                "错误", JOptionPane.WARNING_MESSAGE);
    }

    public void playSound() {
        try {
            if (clip == null) {
                clip = AudioSystem.getClip();
            }

            // 项目根目录/打出来jar包的运行目录
            File file = new File(System.getProperty("user.dir") + "\\sound.wav");
            clip.open(AudioSystem.getAudioInputStream(file));

            // 循环播放
            clip.loop(Clip.LOOP_CONTINUOUSLY);

        } catch (Exception e) {
            ByteArrayOutputStream baoStream = new ByteArrayOutputStream();
            e.printStackTrace(new PrintStream(baoStream));
            log.error(baoStream.toString());
            log.error(e.getMessage());
        }
    }

    public void stopSound() {
        if (clip != null && clip.isRunning()) {
            clip.stop();
            clip.close();
        }
    }

    // ---------------------------------------------------------------------------------------------------------------- Override Method

    @Override
    public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) {
    }

    @Override
    public void nativeKeyPressed(NativeKeyEvent event) {
        boolean isHotKey = this.pressedKeyNoOrder(event.getKeyCode());
        // 成功并且窗口未展示时候
        if (isHotKey && !isWindowShow) {
            String keyTexts = this.getKeyTexts();
            log.info("触发快捷组合键" + keyTexts + "成功...");
            // 窗口标志位设置成功
            isWindowShow = true;
            // 播放声音
            this.playSound();
            // 显示窗口
            this.showWindow(keyTexts);
        }
    }

    @Override
    public void nativeKeyReleased(NativeKeyEvent event) {
        this.releaseKey(event.getKeyCode());
    }

    public static void main(String[] args) {

        // 禁止双击jar包运行
        if (!"hotkey-id".equals(System.getProperty("app.id"))) {
            log.info("不允许通过双击来运行jar文件!!!");
            dialogWindow("不允许通过双击来运行jar文件!!!");
            System.exit(1);
        }

        if (isAppAlreadyRunning()) {
            log.info("无法正常获取到.hotkey.lock锁, 已有示例在运行, 请勿重新启动");
            dialogWindow("已有示例在运行, 请勿重新启动.");
            System.exit(1);
        }
        log.info("可正常获取到文件.hotkey.lock锁, 认为第一次启动.");

        try {
            // 设置日志级别，避免 JNativeHook 的日志输出
            Logger logger = Logger.getLogger(GlobalScreen.class.getPackage().getName());
            logger.setLevel(Level.OFF);

            // 注册全局键盘事件
            GlobalScreen.registerNativeHook();
        } catch (NativeHookException ex) {
            ByteArrayOutputStream baoStream = new ByteArrayOutputStream();
            ex.printStackTrace(new PrintStream(baoStream));
            log.error(baoStream.toString());
            log.error("启动失败 !!! ");
            System.exit(1);
        }

        // 添加 NativeKeyListener
        NativeApplication application = new NativeApplication();
        application.initFlags();
        GlobalScreen.addNativeKeyListener(application);

        log.info("启动成功 !!! ");
    }

    /**
     * 通过.hotkey.lock的锁来判断是否有程序启动过
     * 去掉RandomAccessFile的放入try警告, 放进去执行完会自动释放
     *
     * @return true启动过, 不可再次启动, 反之false
     */
    @SuppressWarnings("all")
    private static boolean isAppAlreadyRunning() {
        try {
            File lockFile = new File(System.getenv("APPDATA") + "\\hotkey\\.hotkey.lock");
            if (!lockFile.getParentFile().exists()) {
                lockFile.getParentFile().mkdirs();
            }
            RandomAccessFile file = new RandomAccessFile(lockFile, "rw");
            FileChannel channel = file.getChannel();
            FileLock lock = channel.tryLock();
            return lock == null;  // 如果锁获取失败，说明已经有实例在运行
        } catch (Exception e) {
            ByteArrayOutputStream baoStream = new ByteArrayOutputStream();
            e.printStackTrace(new PrintStream(baoStream));
            log.error(baoStream.toString());
            return false;  // 处理异常情况，也可以选择抛出异常
        }
    }
}